#include "json_schema.h"
#include "str.h"
#include "file.h"

#include "log.h"


namespace evm {

// type
static const string STRING = "string";
static const string NUMBER = "number";
static const string OBJECT = "object";
static const string ARRAY = "array";
static const string BOOLEAN = "boolean";
static const string NULL_ = "null";
static const string INTEGER = "integer";

static const vector<string> simpleTypes = {STRING, NUMBER, OBJECT, ARRAY, BOOLEAN, NULL_, INTEGER};

#define STRINGIZE_DETAIL(x) #x
#define STRINGIZE(x) STRINGIZE_DETAIL(x)

#define THROW(str) throw invalid_argument(string("invalid value at ")\
+ STRINGIZE(__FILE__) + ":" STRINGIZE(__LINE__) + ", " + __func__ + "\n" + str)

#define THROW_JSON(schema) THROW((schema).dump())

JsonSchema::JsonSchema(const string &file_name)
: JsonSchema(Json::parse(File::read_all(file_name))) {
}

bool JsonSchema::validate_by_schema(const Json &data, const Json &schema) const {
    if (schema.count("type")) {
        const auto& type = schema["type"];

        // lambda begin
        auto validate_by_type = [&data, &schema, this] (const string& type) -> bool {
            if (type == STRING) {
                if (!data.is_string())
                    return false;
                string s = data;
                if (schema.count("maxLength")) {
                    size_t maxLength = schema["maxLength"];
                    if (s.length() > maxLength)
                        return false;
                }
                if (schema.count("minLength")) {
                	size_t minLength = schema["minLength"];
                    if (s.length() < minLength)
                        return false;
                }
                if (schema.count("pattern")) {
                    string pattern = schema["pattern"];
                    if (!res_.at(pattern)->search(s))
                        return false;
                }
            }
            else if ((type == NUMBER) || (type == INTEGER)) {
                if (!data.is_number())
                    return false;
                if (type == INTEGER && !data.is_number_integer())
                    return false;

                double d = data;
                if (schema.count("multipleOf")) {
                    double multipleOf = schema["multipleOf"];
                    double res = d / multipleOf;
                    if (res != int64_t(res))
                        return false;
                }
                if (schema.count("maximum")) {
                    double maximum = schema["maximum"];
                    if (schema.count("exclusiveMaximum") && schema["exclusiveMaximum"]) {
                        if (d >= maximum)
                            return false;
                    }
                    else {
                        if (d > maximum)
                            return false;
                    }
                }
                if (schema.count("minimum")) {
                    double minimum = schema["minimum"];
                    if (schema.count("exclusiveMinimum") && schema["exclusiveMinimum"]) {
                        if (d <= minimum)
                            return false;
                    }
                    else {
                        if (d < minimum)
                            return false;
                    }
                }
            }
            else if (type == BOOLEAN) {
                if (!data.is_boolean())
                    return false;
            }
            else if (type == NULL_) {
                if (!data.is_null())
                    return false;
            }
            else if (type == OBJECT) {
                if (!data.is_object())
                    return false;

                vector<string> additional_properties;

                Json pattern_properties = Json::object();  // initial empty object
                if (schema.count("patternProperties"))
                    pattern_properties = schema["patternProperties"];

                Json properties = Json::object();  // initial empty object
                if (schema.count("properties"))
                    properties = schema["properties"];

                for (auto d = data.cbegin(); d != data.cend(); ++d) {
                    if (properties.count(d.key())) {
                        if (!validate_by_schema(d.value(), properties[d.key()]))
                            return false;
                    }
                    else {
                        auto pp = pattern_properties.cbegin();
                        for (; pp != pattern_properties.cend(); ++pp) {
                            if (res_.at(pp.key())->search(d.key())) {
                                if (!validate_by_schema(d.value(), pp.value()))
                                    return false;
                                break;
                            }
                        }
                        if (pp == pattern_properties.cend())
                            additional_properties.push_back(d.key());
                    }
                }

                if (schema.count("additionalProperties")) {
                    const auto& ap = schema["additionalProperties"];
                    if (ap.is_boolean()) {
                        if (!ap && !additional_properties.empty())
                            return false;
                    }
                    else {
                        for (const auto& p: additional_properties) {
                            if (!validate_by_schema(data[p], ap))
                                return false;
                        }
                    }
                }

                if (schema.count("required")) {
                    for (const auto& k: schema["required"]) {
                        if (!data.count(k))
                            return false;
                    }
                }

                if (schema.count("minProperties")) {
                	size_t minProperties = schema["minProperties"];
                    if (data.size() < minProperties)
                        return false;
                }
                if (schema.count("maxProperties")) {
                	size_t maxProperties = schema["maxProperties"];
                    if (data.size() > maxProperties)
                        return false;
                }

                if (schema.count("dependencies")) {
                    const auto& dependencies = schema["dependencies"];
                    for (Json::const_iterator it = dependencies.cbegin(); it != dependencies.cend(); ++it) {
                        if (data.count(it.key())) {
                            // Schema dependencies
                            if (it.value().is_object()) {

                                // inject root schema type
                                Json subschema = it.value();
                                subschema["type"] = schema["type"];

                                if (!validate_by_schema(data, subschema))
                                    return false;
                            }
                            //Property dependencies
                            else if (it.value().is_array()) {
                                for (const auto& v: it.value()) {
                                    if (!data.count(v))
                                        return false;
                                }
                            }
                        }
                    }
                }
            }
            else if (type == ARRAY) {
                if (!data.is_array())
                    return false;

                if (schema.count("items")) {
                    const auto& items = schema["items"];
                    if (items.is_object()) {  // List validation
                        for (const auto& d: data) {
                            if (!validate_by_schema(d, items))
                                return false;
                        }
                    }
                    else if (items.is_array()) {  // Tuple validation
                        for (size_t i = 0; i < data.size() && i < items.size(); ++i) {
                            if (!validate_by_schema(data[i], items[i]))
                                return false;
                        }

                        if (schema.count("additionalItems")) {
                            const auto& additionalItems = schema["additionalItems"];
                            if (additionalItems.is_boolean()) {
                                if (!additionalItems) {
                                    if (data.size() > items.size())
                                        return false;
                                }
                            }
                            else {
                                for (size_t i = items.size(); i < data.size(); ++i) {
                                    if (!validate_by_schema(data[i], additionalItems))
                                        return false;
                                }
                            }
                        }
                    }
                }

                if (schema.count("maxItems")) {
                	size_t maxItems = schema["maxItems"];
                    if (data.size() > maxItems)
                        return false;
                }
                if (schema.count("minItems")) {
                	size_t minItems = schema["minItems"];
                    if (data.size() < minItems)
                        return false;
                }
                if (schema.count("uniqueItems")) {
                    if (!validate_unique_items(data))
                        return false;
                }
            }

            return true;
        };  // lambda end

        if (type.is_string()) {
            if (!validate_by_type(type))
                return false;
        }
        else {
            bool valid = false;
            for (const auto& t: type) {
                if (validate_by_type(t)) {
                    valid = true;
                    break;
                }
            }
            if (!valid)
                return false;
        }
    }

    if (schema.count("enum")) {
        const auto& enum_ = schema["enum"];
        if (find(enum_.cbegin(), enum_.cend(), data) == enum_.cend())
            return false;
    }

    if (schema.count("$ref")) {
        string ref = schema["$ref"];
        if (!validate_by_schema(data, refer_schemas_.at(ref)))
            return false;
    }

    if (schema.count("allOf")) {
        const Json& allof = schema["allOf"];

        string type;
        for (const auto& s: allof) {
            if (s.count("type")) {
                // ugly two times of assignments, to avoid ambiguous overloaded function.
                string t = s["type"];
                type = t;
                break;
            }
        }

        for (auto s: allof) {
            if (!s.count("type") && !type.empty())
                s["type"] = type;  // inject schema type

            if (!validate_by_schema(data, s))
                return false;
        }
    }

    if (schema.count("anyOf")) {
        bool any = false;
        for (const auto& s: schema["anyOf"]) {
            if (validate_by_schema(data, s)) {
                any = true;
                break;
            }
        }
        if (!any)
            return false;
    }

    if (schema.count("oneOf")) {
        int valids = 0;
        for (const auto& s: schema["oneOf"]) {
            if (validate_by_schema(data, s)) {
                ++valids;
            }
        }
        if (valids != 1)
            return false;
    }

    if (schema.count("not")) {
        if (validate_by_schema(data, schema["not"]))
            return false;
    }

    return true;
}

bool JsonSchema::validate_unique_items(const Json &items) {
    if (!items.is_array())
        THROW_JSON(items);
    unordered_set<Json> s;
    for (const auto& i: items) {
        if (s.count(i))
            return false;
        s.insert(i);
    }
    return true;
}


void JsonSchema::validate_schema(const Json &schema) {
    if (!schema.is_object())
        THROW_JSON(schema);

    for (Json::const_iterator it = schema.cbegin(); it != schema.cend(); ++it) {
        const auto& key = it.key();
        const auto& value = it.value();

        //TODO: validate uri format
        if (key == "id") { }
        else if (key == "$schema") { }

        else if (key == "title") {
            if (!value.is_string())
                THROW_JSON(value);
        }

        else if (key == "description") {
            if (!value.is_string())
                THROW_JSON(value);
        }

        // ignore the default keyword
        else if (key == "default") { }

        else if (key == "multipleOf") {
            if (!value.is_number())
                THROW_JSON(value);
            double multipleOf = value;
            if (multipleOf <= 0)
                THROW_JSON(value);
        }

        else if (key == "maximum") {
            if (!value.is_number())
                THROW_JSON(value);
        }

        else if (key == "exclusiveMaximum") {
            if (!value.is_boolean())
                THROW_JSON(value);
        }

        else if (key == "minimum") {
            if (!value.is_number())
                THROW_JSON(value);
        }

        else if (key == "exclusiveMinimum") {
            if (!value.is_boolean())
                THROW_JSON(value);
        }

        else if (key == "maxLength")
            validate_nonnegative_integer(value);

        else if (key == "minLength")
            validate_nonnegative_integer(value);

        else if (key == "pattern") {
            if (!value.is_string())
                THROW_JSON(value);
            string pattern = value;
            if (!res_.count(pattern))
                res_[pattern] = make_shared<re::RegexObject>(pattern);
        }

        else if (key == "additionalItems") {
            if (value.is_boolean()) { }
            else if (value.is_object())
                validate_schema(value);
            else
                THROW_JSON(value);
        }

        else if (key == "items") {
            if (value.is_object())
                validate_schema(value);
            else if (value.is_array()) {
                Json items = value;
                if (items.empty())
                    THROW_JSON(items);
                for (const auto &item: items)
                    validate_schema(item);
            }
        }

        else if (key == "maxItems")
            validate_nonnegative_integer(value);

        else if (key == "minItems")
            validate_nonnegative_integer(value);

        else if (key == "uniqueItems") {
            if (!value.is_boolean())
                THROW_JSON(value);
        }

        else if (key == "maxProperties")
            validate_nonnegative_integer(value);

        else if (key == "minProperties")
            validate_nonnegative_integer(value);

        else if (key == "required") {
            if (!value.is_array())
                THROW_JSON(value);
            validate_string_array(value);
        }

        else if (key == "additionalProperties") {
            if (value.is_boolean()) { }
            else if (value.is_object())
                validate_schema(value);
            else
                THROW_JSON(value);
        }

        else if (key == "definitions") {
            if (!value.is_object())
                THROW_JSON(value);
            validate_additional_properties(value);
        }

        else if (key == "properties") {
            if (!value.is_object())
                THROW_JSON(value);
            validate_additional_properties(value);
        }

        else if (key == "patternProperties") {
            if (!value.is_object())
                THROW_JSON(value);
            for (Json::const_iterator it = value.cbegin(); it != value.cend(); ++it) {
                string pattern = it.key();
                if (!res_.count(pattern))
                    res_[pattern] = make_shared<re::RegexObject>(pattern);
                validate_schema(it.value());
            }
        }

        else if (key == "dependencies") {
            if (!value.is_object())
                THROW_JSON(value);
            for (Json::const_iterator d = value.cbegin(); d != value.cend(); ++d) {
                const auto &dependency = d.value();
                if (dependency.is_object())
                    validate_schema(dependency);
                else if (dependency.is_array())
                    validate_string_array(dependency);
                else
                    THROW_JSON(dependency);
            }
        }

        else if (key == "enum") {
            if (!value.is_array())
                THROW_JSON(value);
            if (!validate_unique_items(value))
                THROW_JSON(value);
        }

        else if (key == "type") {
            if (value.is_string()) {
                if (find(simpleTypes.cbegin(), simpleTypes.cend(), value) == simpleTypes.cend())
                    THROW_JSON(value);

            }
            else if (value.is_array()) {
                if (value.empty())
                    THROW_JSON(value);
                for (const auto &t: value) {
                    if (!t.is_string())
                        THROW_JSON(t);
                    string type = t;
                    if (find(simpleTypes.cbegin(), simpleTypes.cend(), type) == simpleTypes.cend())
                        THROW_JSON(t);
                }
                validate_unique_items(value);
            }
            else
                THROW_JSON(value);
        }

        else if (key == "allOf")
            validate_schema_array(value);

        else if (key == "anyOf")
            validate_schema_array(value);

        else if (key == "oneOf")
            validate_schema_array(value);

        else if (key == "not")
            validate_schema(value);

        // now only support referring to current document or another local file,
        // do not support referring to URI.
        else if (key == "$ref") {
            if (!value.is_string())
                THROW_JSON(value);
            string ref = value;
            if (!refer_schemas_.count(ref)){
                if (ref == "#")  // refer to root document
                    refer_schemas_["#"] = schema_;
                else {
                    Json child_schema;
                    if (str::startswith(ref, "#"))
                        child_schema = refer_current_doc(ref, schema_);
                    else
                        child_schema = refer_local_file(ref);
                    validate_schema(child_schema);
                    refer_schemas_[ref] = child_schema;
                }
            }
        }
    }
}

void JsonSchema::validate_schema_array(const Json &items) {
    if (items.empty())
        THROW_JSON(items);
    for (const auto& item: items)
        validate_schema(item);
}

void JsonSchema::validate_string_array(const Json &items) {
    if (items.empty())
        THROW_JSON(items);
    for (const auto& s: items) {
        if (!s.is_string())
            THROW_JSON(s);
    }
    if (!validate_unique_items(items))
        THROW_JSON(items);
}

void JsonSchema::validate_nonnegative_integer(const Json &item) {
    if (!item.is_number_integer())
        THROW_JSON(item);
    int64_t integer = item;
    if (integer < 0)
        THROW_JSON(item);
}

void JsonSchema::validate_additional_properties(const Json &object) {
    for (Json::const_iterator it = object.cbegin(); it != object.cend(); ++it) {
        //log_info("ap: {}", it.key());
        validate_schema(it.value());
    }
}

Json JsonSchema::refer_current_doc(const string &ref_path, const Json &schema) {
    //log_info("ref_path: {}", ref_path);
    vector<string> segs = str::split(ref_path, "/");
    if (segs.size() < 2 || segs[0] != "#" || segs.back() == "")
        THROW(ref_path);
    const Json *child_schema = &schema;
    for (size_t i = 1; i < segs.size(); ++i) {
        if (!child_schema->count(segs[i]))
            THROW_JSON(*child_schema);
        child_schema = &(*child_schema)[segs[i]];
    }
    if (!child_schema->is_object())
        THROW_JSON(*child_schema);
    //log_info("child_schema: {}", (*child_schema).dump());
    return *child_schema;
}

Json JsonSchema::refer_local_file(const string &ref_file) {
    vector<string> segs = str::split(ref_file, "#");
    if (segs.size() < 2)
        THROW(ref_file);
    const string& file_name = segs[0];
    Json schema = Json::parse(File::read_all(file_name));
    return refer_current_doc("#" + segs[1], schema);
}

}
